<?php

namespace think\wework\Api;

use ArrayAccess;
use Exception;
use SimpleXMLElement;
use think\facade\Log;
use think\facade\Request;
use think\wework\Abstracts\WeworkApi;
use think\wework\Crypt\WXBizMsgCrypt;
use think\wework\Message\ReplyMessageInterface;

class Callback extends WeworkApi implements ArrayAccess
{
    private WXBizMsgCrypt $crypt;
    private array $wxMessage = [];
    private static ?Callback $instance = null;

    public static function getInstance(): Callback
    {
        if(!self::$instance instanceof Callback) {
            self::$instance = new self();
        }
        return self::$instance;
    }

    public function __construct()
    {
        parent::__construct();
        $this->crypt = new WXBizMsgCrypt($this->token, $this->aesKey, $this->corpId);
    }

    /**
     * @param string $key
     * @return mixed
     * @throws Exception
     */
    public function get(string $key)
    {
        if (!Request::getContent()) {
            return null;
        }

        if (!$this->offsetExists($key)) {
            $this->initializeAttribute();
        }
        return $this->offsetGet($key);
    }

    /**
     * @return void
     * @throws Exception
     */
    private function initializeAttribute(): void
    {
        $data = '';

        $this->crypt->DecryptMsg(
            Request::get('msg_signature'),
            Request::get('timestamp'),
            Request::get('nonce'),
            Request::getContent(),
            $data
        );
        if ($data) {
            $xml = new SimpleXMLElement($data);
            foreach ($xml as $key => $value) {
                $this->offsetSet($key, $value);
            }
        }
    }

    /**
     * @return string
     */
    private function decryptEchoStr(): string
    {
        $plainText = '';

        $this->crypt->VerifyURL(
            Request::get('msg_signature'),
            Request::get('timestamp'),
            Request::get('nonce'),
            Request::get('echostr'),
            $plainText
        );

        return $plainText;
    }

    /**
     * @param ReplyMessageInterface $replyMessage
     * @return string
     */
    public function reply(ReplyMessageInterface $replyMessage): string
    {
        if (Request::has('echostr')) {
            return $this->decryptEchoStr();
        } else {
            return $this->encryptReply($this->buildReply($replyMessage));
        }
    }

    /**
     * @param string $reply
     * @return string
     */
    private function encryptReply(string $reply): string
    {
        $cipherText = '';

        $this->crypt->EncryptMsg(
            $reply,
            Request::get('timestamp'),
            Request::get('nonce'),
            $cipherText
        );

        return $cipherText;
    }

    /**
     * @param ReplyMessageInterface $replyMessage
     * @return string
     */
    private function buildReply(ReplyMessageInterface $replyMessage): string
    {
        $reply = $replyMessage->formatForReply();

        $reply['ToUserName'] = $this->offsetGet('FromUserName');
        $reply['FromUserName'] = $this->offsetGet('ToUserName');
        $reply['CreateTime'] = (int)$this->offsetGet('CreateTime');

        $element = new SimpleXMLElement('<xml/>');

        $this->arrayToXml($reply, $element);

        $dom = dom_import_simplexml($element);

        return $dom->ownerDocument->saveXML($dom->ownerDocument->documentElement);
    }

    /**
     * @param array $data
     * @param SimpleXMLElement $element
     * @return void
     */
    private function arrayToXml(array $data, SimpleXMLElement &$element): void
    {
        foreach ($data as $key => $value) {
            if (is_numeric($key)) {
                $key = 'item';
            }

            if (is_array($value)) {
                $subNode = $element->addChild($key);
                $this->arrayToXml($value, $subNode);
            } elseif (is_string($value)) {
                $node = dom_import_simplexml($element->addChild($key));
                $no = $node->ownerDocument;
                $node->appendChild($no->createCDATASection($value));
            } else {
                $element->addChild($key, $value);
            }
        }
    }

    public function offsetExists($offset): bool
    {
        return isset($this->wxMessage[$offset]);
    }

    public function offsetGet($offset)
    {
        return $this->wxMessage[$offset];
    }

    public function offsetSet($offset, $value)
    {
        if(is_null($offset)) {
            $this->wxMessage[] = $value;
        } else {
            $this->wxMessage[$offset] = $value;
        }
    }

    public function offsetUnset($offset)
    {
        unset($this->wxMessage[$offset]);
    }
}
